package com.googlecode.cswish.util;

import java.lang.reflect.Method;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.StrutsConstants;

import com.googlecode.cswish.Config;
import com.googlecode.cswish.model.PageElements;
import com.googlecode.cswish.security.SafeManager;
import com.googlecode.cswish.security.auth.UserInfo;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.struts.spring.GenericService;
import com.googlecode.cswish.struts.spring.ObjectFactory;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
import com.opensymphony.xwork2.inject.Inject;

/**
 * Provide the common functions (Sigleton)
 * 
 * @author Jerry.Feng Date: 2008-5-30
 */
public class Functions extends AbstractMap<String, Object> {
	
	private static final Logger logger = Logger.getLogger(Functions.class);

	private static String base;
	
	private long id;
	
	@Inject(StrutsConstants.STRUTS_ACTION_EXTENSION)
	protected String actionExtension;
	
	protected boolean devMode;
	
	@Inject(StrutsConstants.STRUTS_DEVMODE)
    public void setDevMode(String mode) {
        this.devMode = "true".equals(mode);
    }
	
	public boolean isDevMode() {
		return this.devMode;
	}
	
	@Resource
	protected ObjectFactory objectFactory;
	
	@Resource
	protected BeanIntrospector beanIntrospector;
	
	@Resource
	protected OgnlExUtil ognlExUtil;
	
	@Resource
	protected GenericService genericService;
	
	@Resource
	protected Config config;
	
	@Resource
	protected SafeManager safeManager;
	
	public boolean permit(String url, String method) {
		HttpServletRequest request = ServletActionContext.getRequest();
		UserInfo userInfo = safeManager.getLoginUser(ServletActionContext.getRequest());
		if (userInfo == null) {
			return false;
		}
		
		String userId = userInfo.getLoginId();
		int end = url.indexOf('?');
		if (end == -1) {
			end = url.length();
		}
		
		if (url.length() == 0) {
			url = request.getServletPath();
		} else if (url.startsWith(getBase())) {
			url = url.substring(getBase().length(), end);
		} else {
			url = url.substring(0, end);
		}
		
		String path;
		if (method != null) {
			int methodIndex = url.lastIndexOf('.', url.length() - actionExtension.length() - 2);
			path = url.substring(0, methodIndex + 1) + method + '.' + actionExtension;
		} else {
			path = url;
		}
		return safeManager.checkPage(userId, path, null);
	}
	
	public static String getBase() {
		if (base != null) {
			return base;
		}
		
		base = ServletActionContext.getServletContext().getContextPath();
		return base;
	}
	
	public String getActionExtension() {
		return this.actionExtension;
	}
	
	//////////////////////////// collection function //////////////////////////
	public double sum(Collection<?> coll) {
		if (coll == null) {
			return 0;
		}
		
		double sum = 0;
		for (Object item : coll) {
			double value = 0;
			try {
				value = Double.parseDouble(item.toString());
			} catch (Exception ex) {
				logger.debug(ex);
			}
			sum += value;
		}
		return sum;
	}
	
	public List<?> select(Collection<?> coll, String propertyName, Object defaultValue) {
		List<Object> values = new ArrayList<Object>(coll.size());
		for (Object item : coll) {
			if (propertyName != null) {
				item = ognlExUtil.getValue(propertyName, BlankObject.Map, item);
			}
			if (item == null) {
				item = defaultValue;
			}
			values.add(item);
		}
		return values;
	}
	
	public boolean contain(Collection<?> coll, Object value) {
		return coll != null && coll.contains(value);
	}
	
	public String join(Collection<?> coll, String separator) {
		return StringUtils.join(coll, separator);
	}
	
	public String[] toArray(Collection<String> coll) {
		return coll.toArray(new String[coll.size()]);
	}
	
	public boolean isCollection(Object obj) {
		return obj instanceof Collection;
	}
	
	public List<Object> toList(Object obj) {
		if (obj instanceof List) {
			return (List<Object>)obj;
		} else {
			List<Object> list = new ArrayList<Object>(1);
			list.add(obj);
			return list;
		}
	}
	
	///////////////////////////////////////////////////////////////////////////
	
	/////////////////////////////// service function //////////////////////////
	public String getModelName() {
		String className = ActionContext.getContext().getActionInvocation()
			.getAction().getClass().getName();
		String simpleName = beanIntrospector.getSimpleName(className);
		String modelName = beanIntrospector.getModelName(simpleName);
		return modelName;
	}
	
	/**
	 * create the model instance according to current path request
	 * 
	 * @return
	 */
	public Object newModel() {
		return newModel(getModelName());
	}
	
	public Object newModel(String className) {
		return objectFactory.buildModel(className, new HashMap());
	}
	
	public Object loadModel(String id) {
		Object model = newModel();
		ognlExUtil.setValue("id", BlankObject.Map, model, id);
		return genericService.load(model);
	}
	
	public Object service(String name) {
		String serviceName = null;
		try {
			if ("generic".equals(name)) {
				return genericService;
			} else {
				serviceName = beanIntrospector.getServiceName(name);
				return objectFactory.getBean(serviceName, true);
			}
		} catch (Exception ex) {
			logger.error("Fail to create service: " + name + " (" + serviceName + "), please check if the class exists", ex);
			return null;
		}
	}
	
	/**
	 * Call the service method and support null parameter (like Javascript method invoker)
	 * 
	 * @param modelName
	 * @param method
	 * @param parameters
	 * @return
	 */
	public Object service(String modelName, String methodName, List<Object> parameters) {
		Object service = service(modelName);
		
		if (service != null) {
			Method[] methods = service.getClass().getMethods();
			
			Method matchedMethod = null;
			for (Method method : methods) {
				if (methodName.equals(method.getName())) {
					
					matchedMethod = method;
					break;
				}
			}
			
			if (matchedMethod != null) {
				Class<?>[] parameterTypes = matchedMethod.getParameterTypes();

				XWorkConverter converter = ognlExUtil.getXWorkConverter();
				int index = 0;
				Object[] parameterValues = new Object[parameterTypes.length];
				for (Class<?> parameterType : parameterTypes) {
					Object value = null;
					if (parameters != null && parameters.size() > index) {
						value = parameters.get(index);
					}
					
					if (value != null && parameterType.isAssignableFrom(value.getClass())) {
						parameterValues[index] = value;
					} else {
						parameterValues[index] = converter.convertValue(value, parameterType);
					}
					index++;
				}
				
				try {
					return matchedMethod.invoke(service, parameterValues);
				} catch (Exception ex) {
					logger.error("Fail to execute service: " + modelName + '.' + methodName, ex);
				}
			} else {
				logger.error("no matched method is found: " + modelName + '.' + methodName);
			}
		}
		return null;
	}
	///////////////////////////////////////////////////////////////////////////
	
	////////////////////////////// page function //////////////////////////////
	/**
	 * 
	 * @param methodName
	 * @param fieldName
	 * @return the parameter object defined in ParamScope
	 */
	public Object getSessionParameter(String methodName, String fieldName) {
		ActionContext actionContext = ActionContext.getContext();
		Map context = actionContext.getValueStack().getContext();
		PageElements pageStructure = (PageElements) context.get(FrameConstant.STACK_PAGEELEMENTS);
		String key = pageStructure.getAction() + '@' + methodName + '@' + fieldName;
		return actionContext.getSession().get(key);
	}
	
	public boolean isInput(String method) {
		return method != null && method.startsWith(FrameConstant.ACTION_PREPARE);
	}

	public String link(String action) {
		return link(action, null);
	}
	
	public String link(String action, String parameters) {
		return getBase() + linkNoBase(action, parameters);
	}
	
	public String linkNoBase(String action, String parameters) {
		StringBuilder sb = new StringBuilder();
		if (action.length() > 0 && action.charAt(0) != '/') {
			sb.append('/');
		}
		sb.append(action);
		sb.append(".").append(actionExtension);
		if (parameters != null) {
			sb.append('?').append(parameters);
		}
		return sb.toString();
	}
	
	public String linkModel(String modelName, String method) {
		String simpleName = beanIntrospector.getSimpleName(modelName);
		String action = simpleName.replace('.', '/');
		return link(action + '.' + method);
	}
	
	public String linkModelNoBase(String modelName, String method) {
		String simpleName = beanIntrospector.getSimpleName(modelName);
		String action = simpleName.replace('.', '/');
		return linkNoBase(action + '.' + method, null);
	}
	
	public String replaceAction(String action, String newMethod) {
		return action.substring(0, action.lastIndexOf('.') + 1) + newMethod;
	}
	
	/**
	 * get the request page name
	 */
	public String page(String methodName) {
		return page("generic", methodName);
	}
	
	public String page(String simpleClassName, String methodName) {
		StringBuilder sb = new StringBuilder();
		String extension;
		if ("generic".equals(simpleClassName)) {
			sb.append(config.getViewTemplate());
			extension = "ftl";
		} else {
			sb.append(config.getViewDir());
			extension = actionExtension;
		}
		
		sb.append(simpleClassName.replace('.', '/'));
		if (methodName != null && methodName.length() > 0) {
			sb.append('.').append(methodName);
		}
		
		sb.append(".").append(extension);
		return sb.toString();
	}
	
	public String cssLink(String name) {
		return getBase() + css(name);
	}
	
	public String css(String name) {
		return config.getCssDir() + name + ".css";
	}

	@SuppressWarnings("unchecked")
	@Override
	public Set<java.util.Map.Entry<String, Object>> entrySet() {
		return BlankObject.Set;
	}
	
	public Object get(Object key) {
		String name = (String)key;
		return new MValue(name);
	}
	
	/**
	 * Avoid freemarker output the return value
	 * 
	 * @param value
	 */
	public void v(Object value) {
	}
	
	// page safe
	public String getId() {
		return '_' + String.valueOf(id++);
	}
	///////////////////////////////////////////////////////////////////////////
	
	////////////////////////////// debug function /////////////////////////////
	public String debugTag(String info) {
		if (devMode) {
			return "<!--" + info + "-->";
		} else {
			return "";
		}
	}
	
	public void debug(String pageName, Object object) {
		logger.debug(pageName + ':' + object);
	}
	///////////////////////////////////////////////////////////////////////////
	
	////////////////////////////// el support /////////////////////////////////
	// el only support the standard get/set method
	class MValue extends AbstractMap<String, Object> {
		
		private String name;
		
		public MValue(String name) {
			this.name = name;
		}
		
		@Override
		public Set<java.util.Map.Entry<String, Object>> entrySet() {
			return BlankObject.Set;
		}
		
		public Object get(Object key) {
			if (!(key instanceof String)) {
				return null;
			}
			
			String value = (String) key;
			if ("css".equals(name)) {
				return css(value);
			} else if ("cssLink".equals(name)) {
				return cssLink(value);
			} else if ("css".equals(name)) {
				return css(value);
			} else if ("isInput".equals(name)) {
				return isInput(value);
			} else if ("link".equals(name)) {
				return link(value);
			} else if ("newModel".equals(name)) {
				return newModel(value);
			} else if ("page".equals(name)) {
				return page(value);
			}
			return null;
		}
		
		public String toString() {
			return String.valueOf(get(null));
		}
	}
}